/***********************************************************************************************************************
*  OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
*  See also https://openstudio.net/license
***********************************************************************************************************************/

#include <gtest/gtest.h>

#include "ModelFixture.hpp"

#include "../AirConditionerVariableRefrigerantFlow.hpp"
#include "../AirLoopHVAC.hpp"
#include "../AirLoopHVACZoneSplitter.hpp"
#include "../AirLoopHVACZoneMixer.hpp"
#include "../AirTerminalSingleDuctConstantVolumeNoReheat.hpp"
#include "../BoilerHotWater.hpp"
#include "../CoolingTowerSingleSpeed.hpp"
#include "../CoilCoolingDXSingleSpeed.hpp"
#include "../CoilHeatingDXSingleSpeed.hpp"
#include "../CoilHeatingWater.hpp"
#include "../CurveBiquadratic.hpp"
#include "../CurveQuadratic.hpp"
#include "../DesignSpecificationOutdoorAir.hpp"
#include "../ElectricEquipment.hpp"
#include "../ElectricEquipmentDefinition.hpp"
#include "../FanConstantVolume.hpp"
#include "../LifeCycleCost.hpp"
#include "../LifeCycleCost_Impl.hpp"
#include "../Lights.hpp"
#include "../LightsDefinition.hpp"
#include "../Model_Impl.hpp"
#include "../Node.hpp"
#include "../Node_Impl.hpp"
#include "../PlantLoop.hpp"
#include "../PortList.hpp"
#include "../PortList_Impl.hpp"
#include "../ScheduleCompact.hpp"
#include "../ScheduleRuleset.hpp"
#include "../ScheduleRuleset_Impl.hpp"
#include "../ScheduleConstant.hpp"
#include "../ScheduleConstant_Impl.hpp"
#include "../SetpointManagerSingleZoneReheat.hpp"
#include "../SizingZone.hpp"
#include "../SizingZone_Impl.hpp"
#include "../Space.hpp"
#include "../SpaceType.hpp"
#include "../SpaceInfiltrationDesignFlowRate.hpp"
#include "../SpaceInfiltrationEffectiveLeakageArea.hpp"
#include "../StraightComponent.hpp"
#include "../ThermalZone.hpp"
#include "../ThermalZone_Impl.hpp"
#include "../ThermostatSetpointDualSetpoint.hpp"
#include "../ThermostatSetpointDualSetpoint_Impl.hpp"
#include "../ZoneControlHumidistat.hpp"
#include "../ZoneControlHumidistat_Impl.hpp"
#include "../ZoneControlContaminantController.hpp"
#include "../ZoneControlContaminantController_Impl.hpp"
#include "../ZoneHVACPackagedTerminalAirConditioner.hpp"
#include "../ZoneHVACPackagedTerminalHeatPump.hpp"
#include "../ZoneHVACTerminalUnitVariableRefrigerantFlow.hpp"
#include "../ZoneHVACUnitHeater.hpp"
#include "../ZoneHVACUnitHeater_Impl.hpp"

#include "../../utilities/core/Containers.hpp"
#include "../../utilities/geometry/Point3d.hpp"

using namespace openstudio;
using namespace openstudio::model;

TEST_F(ModelFixture, ThermalZone_Spaces) {
  Model model;
  ThermalZone thermalZone(model);
  Space space1(model);
  Space space2(model);

  EXPECT_FALSE(space1.thermalZone());
  EXPECT_FALSE(space2.thermalZone());
  EXPECT_EQ(0u, thermalZone.spaces().size());

  EXPECT_TRUE(space1.setThermalZone(thermalZone));
  ASSERT_TRUE(space1.thermalZone());
  EXPECT_EQ(thermalZone.handle(), space1.thermalZone()->handle());
  EXPECT_FALSE(space2.thermalZone());
  EXPECT_EQ(1u, thermalZone.spaces().size());

  ASSERT_TRUE(space1.thermalZone());
  EXPECT_EQ(thermalZone.handle(), space1.thermalZone()->handle());
  EXPECT_TRUE(space2.setThermalZone(thermalZone));
  ASSERT_TRUE(space2.thermalZone());
  EXPECT_EQ(thermalZone.handle(), space2.thermalZone()->handle());
  EXPECT_EQ(2u, thermalZone.spaces().size());
}

TEST_F(ModelFixture, ThermalZone_Remove) {
  Model model;
  ThermalZone zone1(model);
  AirLoopHVAC airLoopHVAC(model);
  std::vector<ModelObject> modelObjects;

  modelObjects = airLoopHVAC.demandComponents();
  EXPECT_EQ(5u, modelObjects.size());

  EXPECT_TRUE(airLoopHVAC.addBranchForZone(zone1, boost::none));

  modelObjects = airLoopHVAC.demandComponents();
  EXPECT_EQ(7u, modelObjects.size());
  ThermalZoneVector thermalZones = subsetCastVector<ThermalZone>(modelObjects);
  ASSERT_EQ(1u, thermalZones.size());
  EXPECT_EQ(zone1.handle(), thermalZones[0].handle());

  auto mo = zone1.returnAirModelObject();
  EXPECT_TRUE(mo);

  AirLoopHVAC airLoopHVAC2(model);
  airLoopHVAC2.multiAddBranchForZone(zone1);

  auto mos = zone1.returnAirModelObjects();
  EXPECT_EQ(2u, mos.size());

  ASSERT_NO_THROW(zone1.remove());

  modelObjects = airLoopHVAC.demandComponents();
  EXPECT_EQ(5u, modelObjects.size());

  modelObjects = airLoopHVAC2.demandComponents();
  EXPECT_EQ(5u, modelObjects.size());
}

TEST_F(ModelFixture, ThermalZone_AddToNode_SPM) {
  Model m;
  AirLoopHVAC airLoopHVAC(m);
  ThermalZone thermalZone(m);
  ThermalZone thermalZone2(m);
  ScheduleCompact s(m);
  AirTerminalSingleDuctConstantVolumeNoReheat singleDuctTerminal(m, s);
  SetpointManagerSingleZoneReheat spm(m);

  Node outletNode = airLoopHVAC.supplyOutletNode();
  spm.addToNode(outletNode);
  EXPECT_FALSE(spm.controlZone());

  Node inletNode = airLoopHVAC.zoneSplitter().lastOutletModelObject()->cast<Node>();

  EXPECT_TRUE(thermalZone.addToNode(inletNode));

  EXPECT_TRUE(spm.controlZone());
  EXPECT_EQ(thermalZone, spm.controlZone().get());

  EXPECT_TRUE(airLoopHVAC.addBranchForZone(thermalZone2, singleDuctTerminal));

  EXPECT_TRUE(spm.controlZone());
  EXPECT_EQ(thermalZone, spm.controlZone().get());
  EXPECT_NE(thermalZone2, spm.controlZone().get());
}

TEST_F(ModelFixture, ThermalZone_sizingZone) {
  Model model;
  ThermalZone zone1(model);

  ASSERT_NO_THROW(zone1.sizingZone());
}

/* Tests that you cannot set Fractions that sum to greater than 1 */
TEST_F(ModelFixture, ThermalZone_FractionofZoneControlledbyDaylightingControl_PriSecLimits) {
  Model m;
  ThermalZone z(m);

  ASSERT_TRUE(z.setFractionofZoneControlledbyPrimaryDaylightingControl(0.5));
  ASSERT_TRUE(z.setFractionofZoneControlledbySecondaryDaylightingControl(0.5));
  ASSERT_FALSE(z.setFractionofZoneControlledbySecondaryDaylightingControl(0.75));
  ASSERT_FALSE(z.setFractionofZoneControlledbyPrimaryDaylightingControl(0.75));
}

TEST_F(ModelFixture, CombinedInfiltration) {
  Model model;
  ThermalZone thermalZone(model);

  // 100 m^2
  std::vector<Point3d> points{
    {0, 10, 0},
    {10, 10, 0},
    {10, 0, 0},
    {0, 0, 0},
  };

  // 100 m^2, 200 m^2
  boost::optional<Space> space1 = Space::fromFloorPrint(points, 2, model);
  ASSERT_TRUE(space1);
  EXPECT_EQ(200.0, space1->volume());
  space1->setThermalZone(thermalZone);

  SpaceInfiltrationDesignFlowRate infiltration1(model);
  infiltration1.setAirChangesperHour(1.0);
  infiltration1.setSpace(*space1);

  // 100 m^2, 300 m^2
  boost::optional<Space> space2 = Space::fromFloorPrint(points, 3, model);
  ASSERT_TRUE(space2);
  EXPECT_EQ(300.0, space2->volume());
  space2->setThermalZone(thermalZone);

  SpaceInfiltrationDesignFlowRate infiltration2(model);
  infiltration2.setAirChangesperHour(1.0);
  infiltration2.setSpace(*space2);

  boost::optional<Space> newSpace = thermalZone.combineSpaces();
  ASSERT_TRUE(newSpace);
  std::vector<SpaceInfiltrationDesignFlowRate> spaceInfiltrationDesignFlowRates = newSpace->spaceInfiltrationDesignFlowRates();
  ASSERT_EQ(2u, spaceInfiltrationDesignFlowRates.size());
  EXPECT_EQ("Flow/Space", spaceInfiltrationDesignFlowRates[0].designFlowRateCalculationMethod());
  ASSERT_TRUE(spaceInfiltrationDesignFlowRates[0].designFlowRate());
  EXPECT_EQ("Flow/Space", spaceInfiltrationDesignFlowRates[1].designFlowRateCalculationMethod());
  ASSERT_TRUE(spaceInfiltrationDesignFlowRates[1].designFlowRate());

  double totalRate = 0;
  totalRate += spaceInfiltrationDesignFlowRates[0].designFlowRate().get();
  totalRate += spaceInfiltrationDesignFlowRates[1].designFlowRate().get();
  EXPECT_EQ(500.0 / 3600.0, totalRate);
}

TEST_F(ModelFixture, ThermalZone_ThermalZone) {
  ::testing::FLAGS_gtest_death_test_style = "threadsafe";

  ASSERT_EXIT(
    {
      Model model;

      ThermalZone thermalZone(model);

      exit(0);
    },
    ::testing::ExitedWithCode(0), "");
}

TEST_F(ModelFixture, ThermalZone_equipment) {
  Model model;

  ThermalZone thermalZone(model);

  AirLoopHVAC airLoopHVAC(model);

  ScheduleCompact scheduleCompact(model);
  AirTerminalSingleDuctConstantVolumeNoReheat singleDuctTerminal(model, scheduleCompact);

  airLoopHVAC.addBranchForZone(thermalZone, singleDuctTerminal);

  EXPECT_EQ(1u, thermalZone.equipment().size());

  FanConstantVolume fan(model, scheduleCompact);

  CoilHeatingWater heatingCoil(model, scheduleCompact);

  CurveBiquadratic coolingCurveFofTemp = CurveBiquadratic(model);
  coolingCurveFofTemp.setCoefficient1Constant(0.42415);
  coolingCurveFofTemp.setCoefficient2x(0.04426);
  coolingCurveFofTemp.setCoefficient3xPOW2(-0.00042);
  coolingCurveFofTemp.setCoefficient4y(0.00333);
  coolingCurveFofTemp.setCoefficient5yPOW2(-0.00008);
  coolingCurveFofTemp.setCoefficient6xTIMESY(-0.00021);
  coolingCurveFofTemp.setMinimumValueofx(17.0);
  coolingCurveFofTemp.setMaximumValueofx(22.0);
  coolingCurveFofTemp.setMinimumValueofy(13.0);
  coolingCurveFofTemp.setMaximumValueofy(46.0);

  CurveQuadratic coolingCurveFofFlow = CurveQuadratic(model);
  coolingCurveFofFlow.setCoefficient1Constant(0.77136);
  coolingCurveFofFlow.setCoefficient2x(0.34053);
  coolingCurveFofFlow.setCoefficient3xPOW2(-0.11088);
  coolingCurveFofFlow.setMinimumValueofx(0.75918);
  coolingCurveFofFlow.setMaximumValueofx(1.13877);

  CurveBiquadratic energyInputRatioFofTemp = CurveBiquadratic(model);
  energyInputRatioFofTemp.setCoefficient1Constant(1.23649);
  energyInputRatioFofTemp.setCoefficient2x(-0.02431);
  energyInputRatioFofTemp.setCoefficient3xPOW2(0.00057);
  energyInputRatioFofTemp.setCoefficient4y(-0.01434);
  energyInputRatioFofTemp.setCoefficient5yPOW2(0.00063);
  energyInputRatioFofTemp.setCoefficient6xTIMESY(-0.00038);
  energyInputRatioFofTemp.setMinimumValueofx(17.0);
  energyInputRatioFofTemp.setMaximumValueofx(22.0);
  energyInputRatioFofTemp.setMinimumValueofy(13.0);
  energyInputRatioFofTemp.setMaximumValueofy(46.0);

  CurveQuadratic energyInputRatioFofFlow = CurveQuadratic(model);
  energyInputRatioFofFlow.setCoefficient1Constant(1.20550);
  energyInputRatioFofFlow.setCoefficient2x(-0.32953);
  energyInputRatioFofFlow.setCoefficient3xPOW2(0.12308);
  energyInputRatioFofFlow.setMinimumValueofx(0.75918);
  energyInputRatioFofFlow.setMaximumValueofx(1.13877);

  CurveQuadratic partLoadFraction = CurveQuadratic(model);
  partLoadFraction.setCoefficient1Constant(0.77100);
  partLoadFraction.setCoefficient2x(0.22900);
  partLoadFraction.setCoefficient3xPOW2(0.0);
  partLoadFraction.setMinimumValueofx(0.0);
  partLoadFraction.setMaximumValueofx(1.0);

  CoilCoolingDXSingleSpeed coolingCoil = CoilCoolingDXSingleSpeed(model, scheduleCompact, coolingCurveFofTemp, coolingCurveFofFlow,
                                                                  energyInputRatioFofTemp, energyInputRatioFofFlow, partLoadFraction);

  ZoneHVACPackagedTerminalAirConditioner ptac(model, scheduleCompact, fan, heatingCoil, coolingCoil);

  EXPECT_TRUE(ptac.addToThermalZone(thermalZone));

  EXPECT_EQ(2u, thermalZone.equipment().size());
}

TEST_F(ModelFixture, ThermalZone_LoadDistributionScheme) {
  Model model;

  ThermalZone thermalZone(model);

  EXPECT_EQ("SequentialLoad", thermalZone.loadDistributionScheme());

  thermalZone.setLoadDistributionScheme("UniformLoad");
  EXPECT_EQ("UniformLoad", thermalZone.loadDistributionScheme());

  thermalZone.setLoadDistributionScheme("InvalidChoice");
  EXPECT_EQ("UniformLoad", thermalZone.loadDistributionScheme());
}

TEST_F(ModelFixture, ThermalZone_Cost) {
  Model model;
  ThermalZone thermalZone(model);

  boost::optional<LifeCycleCost> cost = LifeCycleCost::createLifeCycleCost("Controls Upgrade", thermalZone, 10.0, "CostPerEach", "Operation", 0, 10);
  ASSERT_TRUE(cost);
  EXPECT_EQ(0, cost->repeatPeriodYears());
  EXPECT_TRUE(cost->isRepeatPeriodYearsDefaulted());
  EXPECT_EQ(10, cost->yearsFromStart());
  EXPECT_FALSE(cost->isYearsFromStartDefaulted());

  EXPECT_DOUBLE_EQ(10.0, cost->totalCost());

  thermalZone.setMultiplier(4);

  EXPECT_DOUBLE_EQ(40.0, cost->totalCost());

  cost = LifeCycleCost::createLifeCycleCost("Controls Upgrade", thermalZone, 10.0, "CostPerArea", "Operation", 0, 10);
  EXPECT_TRUE(cost);
  cost = LifeCycleCost::createLifeCycleCost("Controls Upgrade", thermalZone, 10.0, "CostPerThermalZone", "Operation", 0, 10);
  EXPECT_FALSE(cost);

  EXPECT_EQ(2u, thermalZone.lifeCycleCosts().size());

  thermalZone.remove();

  EXPECT_EQ(0u, model.getConcreteModelObjects<LifeCycleCost>().size());
}

TEST_F(ModelFixture, ThermalZone_CombineSpaces_Cost) {
  Model model;

  Point3dVector floorPrint{
    {0, 10, 0},
    {10, 10, 0},
    {10, 0, 0},
    {0, 0, 0},
  };

  boost::optional<Space> space1 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space1);
  boost::optional<Space> space2 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space2);

  boost::optional<LifeCycleCost> cost1 = LifeCycleCost::createLifeCycleCost("Cost", *space1, 1, "CostPerArea", "Construction");
  ASSERT_TRUE(cost1);
  boost::optional<LifeCycleCost> cost2 = LifeCycleCost::createLifeCycleCost("Cost", *space2, 1, "CostPerArea", "Construction");
  ASSERT_TRUE(cost2);

  EXPECT_DOUBLE_EQ(100, cost1->totalCost());
  EXPECT_DOUBLE_EQ(100, cost2->totalCost());

  ThermalZone thermalZone(model);
  space1->setThermalZone(thermalZone);
  space2->setThermalZone(thermalZone);

  EXPECT_DOUBLE_EQ(100, cost1->totalCost());
  EXPECT_DOUBLE_EQ(100, cost2->totalCost());

  boost::optional<Space> newSpace = thermalZone.combineSpaces();
  ASSERT_TRUE(newSpace);
  ASSERT_EQ(2u, model.getConcreteModelObjects<LifeCycleCost>().size());
  ASSERT_EQ(2u, newSpace->lifeCycleCosts().size());
  EXPECT_EQ("CostPerEach", newSpace->lifeCycleCosts()[0].costUnits());
  EXPECT_EQ(100, newSpace->lifeCycleCosts()[0].totalCost());
  EXPECT_EQ("CostPerEach", newSpace->lifeCycleCosts()[1].costUnits());
  EXPECT_EQ(100, newSpace->lifeCycleCosts()[1].totalCost());
}

TEST_F(ModelFixture, ThermalZone_CombineSpaces_Cost2) {
  Model model;

  Point3dVector floorPrint{
    {0, 10, 0},
    {10, 10, 0},
    {10, 0, 0},
    {0, 0, 0},
  };

  boost::optional<Space> space1 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space1);
  boost::optional<Space> space2 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space2);

  LightsDefinition definition(model);
  Lights lights1(definition);
  lights1.setSpace(*space1);
  Lights lights2(definition);
  lights2.setSpace(*space2);

  boost::optional<LifeCycleCost> cost = LifeCycleCost::createLifeCycleCost("Cost", definition, 1, "CostPerArea", "Construction");
  ASSERT_TRUE(cost);

  EXPECT_DOUBLE_EQ(200, cost->totalCost());

  ThermalZone thermalZone(model);
  space1->setThermalZone(thermalZone);
  space2->setThermalZone(thermalZone);

  EXPECT_DOUBLE_EQ(200, cost->totalCost());

  boost::optional<Space> newSpace = thermalZone.combineSpaces();
  ASSERT_TRUE(newSpace);
  ASSERT_EQ(2u, model.getConcreteModelObjects<LifeCycleCost>().size());
  EXPECT_EQ(0u, newSpace->lifeCycleCosts().size());
  ASSERT_EQ(2u, newSpace->lights().size());
  ASSERT_EQ(1u, newSpace->lights()[0].definition().lifeCycleCosts().size());
  EXPECT_EQ("CostPerEach", newSpace->lights()[0].definition().lifeCycleCosts()[0].costUnits());
  EXPECT_EQ(100, newSpace->lights()[0].definition().lifeCycleCosts()[0].totalCost());
  ASSERT_EQ(1u, newSpace->lights()[1].definition().lifeCycleCosts().size());
  EXPECT_EQ("CostPerEach", newSpace->lights()[1].definition().lifeCycleCosts()[0].costUnits());
  EXPECT_EQ(100, newSpace->lights()[1].definition().lifeCycleCosts()[0].totalCost());
}

TEST_F(ModelFixture, ThermalZone_CombineSpaces_Cost3) {
  Model model;

  Point3dVector floorPrint{
    {0, 10, 0},
    {10, 10, 0},
    {10, 0, 0},
    {0, 0, 0},
  };

  boost::optional<Space> space1 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space1);
  boost::optional<Space> space2 = Space::fromFloorPrint(floorPrint, 3, model);
  ASSERT_TRUE(space2);

  ElectricEquipmentDefinition definition1(model);
  ElectricEquipment equip1(definition1);
  equip1.setSpace(*space1);

  ElectricEquipmentDefinition definition2(model);
  ElectricEquipment equip2(definition2);
  equip2.setSpace(*space2);

  boost::optional<LifeCycleCost> cost1 = LifeCycleCost::createLifeCycleCost("Cost", definition1, 1, "CostPerArea", "Construction");
  ASSERT_TRUE(cost1);
  boost::optional<LifeCycleCost> cost2 = LifeCycleCost::createLifeCycleCost("Cost", definition2, 1, "CostPerArea", "Construction");
  ASSERT_TRUE(cost2);

  EXPECT_DOUBLE_EQ(100, cost1->totalCost());
  EXPECT_DOUBLE_EQ(100, cost2->totalCost());

  ThermalZone thermalZone(model);
  space1->setThermalZone(thermalZone);
  space2->setThermalZone(thermalZone);

  EXPECT_DOUBLE_EQ(100, cost1->totalCost());
  EXPECT_DOUBLE_EQ(100, cost2->totalCost());

  boost::optional<Space> newSpace = thermalZone.combineSpaces();
  ASSERT_TRUE(newSpace);
  ASSERT_EQ(2u, model.getConcreteModelObjects<LifeCycleCost>().size());
  EXPECT_EQ(0u, newSpace->lifeCycleCosts().size());
  ASSERT_EQ(2u, newSpace->electricEquipment().size());
  ASSERT_EQ(1u, newSpace->electricEquipment()[0].definition().lifeCycleCosts().size());
  EXPECT_EQ("CostPerEach", newSpace->electricEquipment()[0].definition().lifeCycleCosts()[0].costUnits());
  EXPECT_EQ(100, newSpace->electricEquipment()[0].definition().lifeCycleCosts()[0].totalCost());
  ASSERT_EQ(1u, newSpace->electricEquipment()[1].definition().lifeCycleCosts().size());
  EXPECT_EQ("CostPerEach", newSpace->electricEquipment()[1].definition().lifeCycleCosts()[0].costUnits());
  EXPECT_EQ(100, newSpace->electricEquipment()[1].definition().lifeCycleCosts()[0].totalCost());
}

TEST_F(ModelFixture, ThermalZone_Clone) {
  Model m;
  ThermalZone thermalZone(m);

  auto zoneAirNode = thermalZone.zoneAirNode();

  ZoneControlHumidistat humidistat(m);
  thermalZone.setZoneControlHumidistat(humidistat);
  ScheduleRuleset humidSchedule(m);
  humidistat.setHumidifyingRelativeHumiditySetpointSchedule(humidSchedule);
  ScheduleRuleset dehumidSchedule(m);
  humidistat.setDehumidifyingRelativeHumiditySetpointSchedule(dehumidSchedule);

  ThermostatSetpointDualSetpoint thermostat(m);
  thermalZone.setThermostat(thermostat);
  ScheduleRuleset coolingSchedule(m);
  thermostat.setCoolingSchedule(coolingSchedule);
  ScheduleRuleset heatingSchedule(m);
  thermostat.setHeatingSchedule(heatingSchedule);

  auto thermalZoneClone = thermalZone.clone(m).cast<ThermalZone>();

  auto humidistatClone = thermalZoneClone.zoneControlHumidistat();
  ASSERT_TRUE(humidistatClone);
  ASSERT_NE(humidistatClone.get(), humidistat);
  auto humidSchedule2 = humidistatClone->humidifyingRelativeHumiditySetpointSchedule();
  ASSERT_TRUE(humidSchedule2);
  ASSERT_EQ(humidSchedule, humidSchedule2.get());
  auto dehumidSchedule2 = humidistatClone->dehumidifyingRelativeHumiditySetpointSchedule();
  ASSERT_TRUE(dehumidSchedule2);
  ASSERT_EQ(dehumidSchedule, dehumidSchedule2.get());

  auto thermostatClone = thermalZoneClone.thermostat();
  ASSERT_TRUE(thermostatClone);
  ASSERT_NE(thermostatClone.get(), thermostat);
  auto coolingSchedule2 = thermostatClone->cast<ThermostatSetpointDualSetpoint>().coolingSetpointTemperatureSchedule();
  ASSERT_TRUE(coolingSchedule2);
  ASSERT_EQ(coolingSchedule, coolingSchedule2.get());
  auto heatingSchedule2 = thermostatClone->cast<ThermostatSetpointDualSetpoint>().heatingSetpointTemperatureSchedule();
  ASSERT_TRUE(heatingSchedule2);
  ASSERT_EQ(heatingSchedule, heatingSchedule2.get());

  EXPECT_FALSE(thermalZoneClone.zoneAirNode().handle().isNull());
  EXPECT_EQ(zoneAirNode, thermalZone.zoneAirNode());
  EXPECT_FALSE(thermalZone.zoneAirNode().handle().isNull());
}

TEST_F(ModelFixture, ThermalZone_SonOfClone) {
  Model model;
  ScheduleConstant schedule(model);
  schedule.setValue(1.0);  // Always on
  FanConstantVolume fan(model, schedule);
  CoilHeatingWater heatingCoil(model, schedule);
  ZoneHVACUnitHeater zoneHVACUnitHeater(model, schedule, fan, heatingCoil);
  ASSERT_FALSE(zoneHVACUnitHeater.thermalZone());

  ThermalZone thermalZone(model);
  auto success = zoneHVACUnitHeater.addToThermalZone(thermalZone);
  ASSERT_TRUE(success);
  ASSERT_TRUE(zoneHVACUnitHeater.thermalZone());

  boost::optional<model::ModelObject> clone1 = zoneHVACUnitHeater.clone(model);
  ASSERT_FALSE(clone1->cast<model::ZoneHVACComponent>().thermalZone());
  ASSERT_TRUE(zoneHVACUnitHeater.thermalZone());
  success = clone1->cast<model::ZoneHVACComponent>().addToThermalZone(thermalZone);
  ASSERT_TRUE(success);
  ASSERT_TRUE(zoneHVACUnitHeater.thermalZone());
  ASSERT_TRUE(clone1->cast<model::ZoneHVACComponent>().thermalZone());

  boost::optional<model::ModelObject> clone2 = zoneHVACUnitHeater.clone(model);
  ASSERT_FALSE(clone2->cast<model::ZoneHVACComponent>().thermalZone());
  ASSERT_TRUE(clone1->cast<model::ZoneHVACComponent>().thermalZone());
  ASSERT_TRUE(zoneHVACUnitHeater.thermalZone());
  success = clone2->cast<model::ZoneHVACComponent>().addToThermalZone(thermalZone);
  ASSERT_TRUE(success);
  ASSERT_TRUE(zoneHVACUnitHeater.thermalZone());
  ASSERT_TRUE(clone1->cast<model::ZoneHVACComponent>().thermalZone());
  ASSERT_TRUE(clone2->cast<model::ZoneHVACComponent>().thermalZone());
}

TEST_F(ModelFixture, ThermalZone_Ports) {
  Model m;
  ThermalZone zone(m);

  auto inletPortList = zone.inletPortList();
  auto exhaustPortList = zone.exhaustPortList();

  auto inletPortListZone = inletPortList.thermalZone();
  auto exhaustPortListZone = exhaustPortList.thermalZone();

  EXPECT_EQ(zone.handle(), inletPortListZone.handle());
  EXPECT_EQ(zone.handle(), exhaustPortListZone.handle());

  zone.remove();
  EXPECT_TRUE(inletPortList.handle().isNull());
  EXPECT_TRUE(exhaustPortList.handle().isNull());
}

TEST_F(ModelFixture, ThermalZone_Thermostat) {
  {
    Model m;

    ThermalZone zone1(m);
    ThermostatSetpointDualSetpoint thermostat1(m);

    EXPECT_TRUE(zone1.setThermostat(thermostat1));
    EXPECT_TRUE(zone1.setThermostat(thermostat1));

    ASSERT_TRUE(thermostat1.thermalZone());
    EXPECT_EQ(zone1, thermostat1.thermalZone().get());

    auto returnvalue = zone1.thermostat();
    ASSERT_TRUE(returnvalue);
    EXPECT_EQ(thermostat1, returnvalue.get());

    ThermalZone zone2(m);

    EXPECT_TRUE(zone2.setThermostat(thermostat1));
    returnvalue = zone2.thermostat();
    ASSERT_TRUE(returnvalue);
    EXPECT_NE(returnvalue.get(), thermostat1);
    auto thermostats = m.getModelObjects<Thermostat>();
    EXPECT_EQ(2u, thermostats.size());

    ThermostatSetpointDualSetpoint themostat2(m);
    EXPECT_TRUE(zone2.setThermostat(themostat2));
    EXPECT_TRUE(returnvalue->handle().isNull());
    ASSERT_TRUE(zone2.thermostat());
    EXPECT_EQ(themostat2.handle(), zone2.thermostat()->handle());
    EXPECT_EQ(2u, m.getModelObjects<Thermostat>().size());
  }

  {
    Model m;
    ThermalZone zone(m);
    ThermostatSetpointDualSetpoint thermostat(m);
    zone.setThermostat(thermostat);
    auto zone2 = zone.clone().cast<model::ThermalZone>();

    ASSERT_TRUE(zone.thermostat());
    ASSERT_TRUE(zone2.thermostat());
    EXPECT_NE(zone.thermostat()->handle(), zone2.thermostat()->handle());
    EXPECT_EQ(2u, m.getModelObjects<model::Thermostat>().size());
  }
}

TEST_F(ModelFixture, ThermalZone_ZoneControlContaminantController) {
  {
    Model m;

    ThermalZone zone1(m);
    ZoneControlContaminantController controller1(m);

    EXPECT_TRUE(zone1.setZoneControlContaminantController(controller1));
    EXPECT_TRUE(zone1.setZoneControlContaminantController(controller1));

    ASSERT_TRUE(controller1.controlledZone());
    EXPECT_EQ(zone1, controller1.controlledZone().get());

    auto returnvalue = zone1.zoneControlContaminantController();
    ASSERT_TRUE(returnvalue);
    EXPECT_EQ(controller1, returnvalue.get());

    ThermalZone zone2(m);

    EXPECT_TRUE(zone2.setZoneControlContaminantController(controller1));
    returnvalue = zone2.zoneControlContaminantController();
    ASSERT_TRUE(returnvalue);
    EXPECT_NE(returnvalue.get(), controller1);
    auto controllers = m.getConcreteModelObjects<ZoneControlContaminantController>();
    EXPECT_EQ(2u, controllers.size());

    ZoneControlContaminantController controller2(m);
    EXPECT_TRUE(zone2.setZoneControlContaminantController(controller2));
    EXPECT_TRUE(returnvalue->handle().isNull());
    ASSERT_TRUE(zone2.zoneControlContaminantController());
    EXPECT_EQ(controller2.handle(), zone2.zoneControlContaminantController()->handle());
    EXPECT_EQ(2u, m.getConcreteModelObjects<ZoneControlContaminantController>().size());
  }

  {
    Model m;
    ThermalZone zone(m);
    ZoneControlContaminantController controller(m);
    zone.setZoneControlContaminantController(controller);
    auto zone2 = zone.clone().cast<model::ThermalZone>();

    ASSERT_TRUE(zone.zoneControlContaminantController());
    ASSERT_TRUE(zone2.zoneControlContaminantController());
    EXPECT_NE(zone.zoneControlContaminantController()->handle(), zone2.zoneControlContaminantController()->handle());
    EXPECT_EQ(2u, m.getConcreteModelObjects<model::ZoneControlContaminantController>().size());
  }
}

TEST_F(ModelFixture, ThermalZone_ZoneControlHumidistat) {
  {
    Model m;

    ThermalZone zone1(m);
    ZoneControlHumidistat controller1(m);

    EXPECT_TRUE(zone1.setZoneControlHumidistat(controller1));
    EXPECT_TRUE(zone1.setZoneControlHumidistat(controller1));

    ASSERT_TRUE(controller1.controlledZone());
    EXPECT_EQ(zone1, controller1.controlledZone().get());

    auto returnvalue = zone1.zoneControlHumidistat();
    ASSERT_TRUE(returnvalue);
    EXPECT_EQ(controller1, returnvalue.get());

    ThermalZone zone2(m);

    EXPECT_TRUE(zone2.setZoneControlHumidistat(controller1));
    returnvalue = zone2.zoneControlHumidistat();
    ASSERT_TRUE(returnvalue);
    EXPECT_NE(returnvalue.get(), controller1);
    auto controllers = m.getConcreteModelObjects<ZoneControlHumidistat>();
    EXPECT_EQ(2u, controllers.size());

    ZoneControlHumidistat controller2(m);
    EXPECT_TRUE(zone2.setZoneControlHumidistat(controller2));
    EXPECT_TRUE(returnvalue->handle().isNull());
    ASSERT_TRUE(zone2.zoneControlHumidistat());
    EXPECT_EQ(controller2.handle(), zone2.zoneControlHumidistat()->handle());
    EXPECT_EQ(2u, m.getConcreteModelObjects<ZoneControlHumidistat>().size());
  }

  {
    Model m;
    ThermalZone zone(m);
    ZoneControlHumidistat controller(m);
    zone.setZoneControlHumidistat(controller);
    auto zone2 = zone.clone().cast<model::ThermalZone>();

    ASSERT_TRUE(zone.zoneControlHumidistat());
    ASSERT_TRUE(zone2.zoneControlHumidistat());
    EXPECT_NE(zone.zoneControlHumidistat()->handle(), zone2.zoneControlHumidistat()->handle());
    EXPECT_EQ(2u, m.getConcreteModelObjects<model::ZoneControlHumidistat>().size());
  }
}

TEST_F(ModelFixture, ThermalZone_AddToNode_NotInSeries) {
  Model m;
  AirLoopHVAC a(m);
  ThermalZone z1(m);
  ThermalZone z2(m);
  ScheduleCompact s(m);
  AirTerminalSingleDuctConstantVolumeNoReheat atu(m, s);

  AirLoopHVACZoneMixer mixer = a.zoneMixer();
  AirLoopHVACZoneSplitter splitter = a.zoneSplitter();

  EXPECT_EQ(5u, a.demandComponents().size());

  EXPECT_TRUE(a.addBranchForHVACComponent(atu));
  EXPECT_EQ(7u, a.demandComponents().size());

  // 9 components:
  // Inlet -- (Mixer) -- Node ----- ATU ----- Node ---- ThermalZone ----- Node ----- (SPlitter) -------- OutletNode

  //            -----o-----ATU-----o-----TZ-----o-----
  // -----o-----|                                    |-----o------

  Node connectingNode = mixer.lastInletModelObject()->cast<Node>();
  EXPECT_TRUE(z1.multiAddToNode(connectingNode));
  EXPECT_EQ(9u, a.demandComponents().size());

  // Try to add Zone 1 twice in series
  connectingNode = mixer.lastInletModelObject()->cast<Node>();
  EXPECT_FALSE(z1.multiAddToNode(connectingNode));
  EXPECT_EQ(9u, a.demandComponents().size());

  // Try to add Zone 2 in series with zone 1
  connectingNode = mixer.lastInletModelObject()->cast<Node>();
  EXPECT_FALSE(z2.multiAddToNode(connectingNode));
  EXPECT_EQ(9u, a.demandComponents().size());

  // TRY WITH PLENUMS NOW
  ThermalZone supplyPlenumZone(m);
  ThermalZone returnPlenumZone(m);

  EXPECT_TRUE(supplyPlenumZone.canBePlenum());
  EXPECT_TRUE(z1.setSupplyPlenum(supplyPlenumZone));
  // Added one one and the plenum
  EXPECT_EQ(11u, a.demandComponents().size());

  // Try to add Zone 2 in series with zone 1
  connectingNode = mixer.lastInletModelObject()->cast<Node>();
  EXPECT_FALSE(z2.multiAddToNode(connectingNode));
  EXPECT_EQ(11u, a.demandComponents().size());

  EXPECT_TRUE(returnPlenumZone.canBePlenum());
  EXPECT_TRUE(z1.setReturnPlenum(returnPlenumZone));
  // Added one node and the plenum
  EXPECT_EQ(13u, a.demandComponents().size());

  // Try to add Zone 2 in series with zone 1
  connectingNode = mixer.lastInletModelObject()->cast<Node>();
  EXPECT_FALSE(z2.multiAddToNode(connectingNode));
  EXPECT_EQ(13u, a.demandComponents().size());
}

TEST_F(ModelFixture, ThermalZone_DaylightingControlsAvailabilitySchedule) {
  Model m;
  ThermalZone z(m);

  EXPECT_FALSE(z.daylightingControlsAvailabilitySchedule());

  auto schedule = m.alwaysOffDiscreteSchedule();

  EXPECT_TRUE(z.setDaylightingControlsAvailabilitySchedule(schedule));
  ASSERT_TRUE(z.daylightingControlsAvailabilitySchedule());
  EXPECT_EQ(schedule, z.daylightingControlsAvailabilitySchedule().get());

  z.resetDaylightingControlsAvailabilitySchedule();
  EXPECT_FALSE(z.daylightingControlsAvailabilitySchedule());
}

TEST_F(ModelFixture, ThermalZone_HeatCoolFuelTypes) {

  Model m;

  FanConstantVolume fan(m);
  CoilHeatingDXSingleSpeed hc(m);
  CoilCoolingDXSingleSpeed cc(m);
  CoilHeatingWater supHC(m);
  auto alwaysOn = m.alwaysOnDiscreteSchedule();
  ZoneHVACPackagedTerminalHeatPump pthp(m, alwaysOn, fan, hc, cc, supHC);

  PlantLoop pthpPlantLoop(m);
  BoilerHotWater b(m);
  EXPECT_TRUE(b.setFuelType("Propane"));
  EXPECT_TRUE(pthpPlantLoop.addSupplyBranchForComponent(b));
  EXPECT_TRUE(pthpPlantLoop.addDemandBranchForComponent(supHC));

  PlantLoop vrfPlantLoop(m);
  CoolingTowerSingleSpeed ct(m);
  AirConditionerVariableRefrigerantFlow vrfSystem(m);
  EXPECT_TRUE(vrfSystem.setFuelType("NaturalGas"));
  EXPECT_TRUE(vrfPlantLoop.addSupplyBranchForComponent(ct));
  EXPECT_TRUE(vrfPlantLoop.addDemandBranchForComponent(vrfSystem));
  EXPECT_EQ("WaterCooled", vrfSystem.condenserType());

  ZoneHVACTerminalUnitVariableRefrigerantFlow vrfTerminal(m, false);
  EXPECT_TRUE(vrfSystem.addTerminal(vrfTerminal));

  ThermalZone z(m);
  EXPECT_TRUE(vrfTerminal.addToThermalZone(z));
  EXPECT_TRUE(pthp.addToThermalZone(z));

  EXPECT_EQ(2, z.equipment().size());

  // VRF Terminal
  {
    // VRF PlantLoop: it has only the Cooling tower
    EXPECT_EQ(ComponentType(ComponentType::Cooling), ct.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity}, ct.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({}, ct.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({}, ct.appGHeatingFuelTypes()));

    EXPECT_EQ(ComponentType(ComponentType::Cooling), vrfPlantLoop.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity}, vrfPlantLoop.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({}, vrfPlantLoop.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({}, vrfPlantLoop.appGHeatingFuelTypes()));

    // VRFSystem: this is the fuelType + the PlantLoop if any
    EXPECT_EQ(ComponentType(ComponentType::Both), vrfSystem.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Gas}, vrfSystem.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Gas}, vrfSystem.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel}, vrfSystem.appGHeatingFuelTypes()));

    // vrfTerminal should return Electric/HeatPump + whatever its VRFSystem is doing
    EXPECT_EQ(ComponentType(ComponentType::Both), vrfTerminal.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Gas}, vrfTerminal.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Gas}, vrfTerminal.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel, AppGFuelType::HeatPump}, vrfTerminal.appGHeatingFuelTypes()));
  }

  // PTHP
  {
    // Cooling Coil
    EXPECT_EQ(ComponentType(ComponentType::Cooling), cc.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity}, cc.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({}, cc.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({}, cc.appGHeatingFuelTypes()));

    // Heating Coil
    EXPECT_EQ(ComponentType(ComponentType::Heating), hc.componentType());
    EXPECT_TRUE(testFuelTypeEquality({}, hc.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity}, hc.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::HeatPump}, hc.appGHeatingFuelTypes()));

    // Supplemental Heating Coil: this is a CoilHeatingWater to it grabs the plantloop
    {
      // Boiler is Propane
      EXPECT_EQ(ComponentType(ComponentType::Heating), b.componentType());
      EXPECT_TRUE(testFuelTypeEquality({}, b.coolingFuelTypes()));
      EXPECT_TRUE(testFuelTypeEquality({FuelType::Propane}, b.heatingFuelTypes()));
      EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel}, b.appGHeatingFuelTypes()));

      EXPECT_EQ(ComponentType(ComponentType::Heating), pthpPlantLoop.componentType());
      EXPECT_TRUE(testFuelTypeEquality({}, pthpPlantLoop.coolingFuelTypes()));
      EXPECT_TRUE(testFuelTypeEquality({FuelType::Propane}, pthpPlantLoop.heatingFuelTypes()));
      EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel}, pthpPlantLoop.appGHeatingFuelTypes()));

      EXPECT_EQ(ComponentType(ComponentType::Heating), supHC.componentType());
      EXPECT_TRUE(testFuelTypeEquality({}, supHC.coolingFuelTypes()));
      EXPECT_TRUE(testFuelTypeEquality({FuelType::Propane}, supHC.heatingFuelTypes()));
      EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel}, supHC.appGHeatingFuelTypes()));
    }

    // PTHP is the sum of Cooling + Heating + supHC coils
    EXPECT_EQ(ComponentType(ComponentType::Both), pthp.componentType());
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity}, pthp.coolingFuelTypes()));
    EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Propane}, pthp.heatingFuelTypes()));
    EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel, AppGFuelType::HeatPump}, pthp.appGHeatingFuelTypes()));
  }

  // Zone checks the zone equipment
  EXPECT_EQ(ComponentType(ComponentType::Both), z.componentType());
  EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Gas}, z.coolingFuelTypes()));
  EXPECT_TRUE(testFuelTypeEquality({FuelType::Electricity, FuelType::Gas, FuelType::Propane}, z.heatingFuelTypes()));
  EXPECT_TRUE(testAppGFuelTypeEquality({AppGFuelType::Fuel, AppGFuelType::HeatPump}, z.appGHeatingFuelTypes()));
}

TEST_F(ModelFixture, ThermalZone_SpacesWithDSOA) {
  Model m;
  ThermalZone z(m);
  Space space1(m);
  Space space2(m);

  EXPECT_TRUE(space1.setThermalZone(z));
  EXPECT_TRUE(space2.setThermalZone(z));
  EXPECT_TRUE(z.spacesWithDesignSpecificationOutdoorAir().empty());

  auto compareSpacesWithDSOA = [&](const std::vector<Space>& expectedSpaces) -> bool {
    auto spaces = z.spacesWithDesignSpecificationOutdoorAir();
    EXPECT_EQ(expectedSpaces.size(), spaces.size());

    std::vector<std::string> expectedSpaceNames;
    expectedSpaceNames.reserve(expectedSpaces.size());
    std::transform(expectedSpaces.cbegin(), expectedSpaces.cend(), std::back_inserter(expectedSpaceNames),
                   [](const auto& s) { return s.nameString(); });
    std::sort(expectedSpaceNames.begin(), expectedSpaceNames.end());

    std::vector<std::string> spaceNames;
    spaceNames.reserve(spaces.size());
    std::transform(spaces.cbegin(), spaces.cend(), std::back_inserter(spaceNames), [](const auto& s) { return s.nameString(); });
    std::sort(spaceNames.begin(), spaceNames.end());
    EXPECT_EQ(expectedSpaceNames, spaceNames);
    return expectedSpaceNames == spaceNames;
  };

  DesignSpecificationOutdoorAir dsoa_space1(m);
  EXPECT_TRUE(space1.setDesignSpecificationOutdoorAir(dsoa_space1));
  EXPECT_EQ(1, z.spacesWithDesignSpecificationOutdoorAir().size());
  compareSpacesWithDSOA({space1});

  SpaceType space_type(m);
  DesignSpecificationOutdoorAir dsoa_sp(m);
  EXPECT_TRUE(space_type.setDesignSpecificationOutdoorAir(dsoa_sp));
  EXPECT_TRUE(space1.setSpaceType(space_type));
  EXPECT_TRUE(space2.setSpaceType(space_type));

  EXPECT_EQ(dsoa_space1, space1.designSpecificationOutdoorAir().get());
  EXPECT_EQ(dsoa_sp, space2.designSpecificationOutdoorAir().get());

  EXPECT_EQ(2, z.spacesWithDesignSpecificationOutdoorAir().size());
  compareSpacesWithDSOA({space1, space2});
}
